package binding

import (
	"reflect"

	"gitee.com/go-errors/errors"
	"gitee.com/go-wena/env"
)

type binder struct {
	h Source
	q Source
}

func (binder binder) Bind(src Source, out interface{}) (err error) {
	var rv reflect.Value
	if v, ok := out.(reflect.Value); ok {
		rv = v
	} else {
		rv = reflect.Indirect(reflect.ValueOf(out))
	}

	return binder.bind(src, rv, rv.Type())
}

func (binder binder) bind(src Source, rv reflect.Value, rt reflect.Type) (err error) {
	switch rt.Kind() {
	case reflect.Ptr:
		if rt == typeFileHeader { //*multipart.FileHeader
			if fs, e := src.File(); e == nil {
				rv.Set(reflect.ValueOf(fs))
			} else if !errors.Is(e, ErrNoValue) {
				err = e
			}
			return
		}
		if rv.IsNil() {
			rv.Set(newValue(rt))
		}
		return binder.bind(src, rv.Elem(), rt.Elem())
	case reflect.Struct:
		if rt == typeTime { //time.Time
			if x, e := src.Time(); e == nil {
				rv.Set(reflect.ValueOf(x))
			} else if !errors.Is(e, ErrNoValue) {
				err = e
			}
			return
		}

		cs := structCache.get(rt)
		var applied bool
		for _, c := range cs {
			applied = false
			for _, pn := range c.names {
				if applied {
					break
				}
				source := src
				switch pn.p {
				case posB:
					source = src
				case posH:
					source = binder.h
				case posQ:
					source = binder.q
				case posE:
					source = stringValue(env.GetString(pn.n))
				default:
					continue
				}

				if source == nil {
					continue
				}

				if sub, ex := source.Get(pn.n); ex != nil {
					if errors.Is(ex, ErrNoValue) {
						continue
					}
					err = errors.Multi(err, ex)
				} else {
					rfv := rv.Field(c.i)
					if ex := binder.bind(sub, rfv, c.rft); ex != nil {
						err = errors.Multi(err, ex)
					} else {
						applied = true
					}
				}
			}
		}
	case reflect.Map:
		rkt := rt.Key()
		rvt := rt.Elem()
		rv.Set(reflect.MakeMap(rt))
		src.Map(func(key string, src Source) {
			rmk := newValue(rkt)
			if ex := binder.bind(stringValue(key), rmk, rkt); ex != nil {
				err = errors.Multi(err, ex)
				return
			}

			rmv := newValue(rvt)
			if ex := binder.bind(src, rmv, rvt); ex != nil {
				err = errors.Multi(err, ex)
				return
			}
			rv.SetMapIndex(rmk, rmv)
		})
	case reflect.Slice:
		if rt.Elem() == typeFileHeader { //[]*multipart.FileHeader
			if fs, ex := src.Files(); ex == nil {
				rv.Set(reflect.ValueOf(fs))
			} else if !errors.Is(ex, ErrNoValue) {
				err = ex
			}
			return
		}
		fallthrough
	case reflect.Array:
		rk := rt.Kind()
		initFunc := func(size int) {
			if size > 0 && rk == reflect.Slice {
				rv.Set(reflect.MakeSlice(rt, size, size))
			}
		}

		src.Array(func(index int, src Source) bool {
			if index < rv.Len() {
				if ex := binder.bind(src, rv.Index(index), rt.Elem()); ex != nil {
					err = errors.Multi(err, ex)
				}
			}
			return index < rv.Len()
		}, initFunc)
	case reflect.String:
		if x, e := src.String(); e != nil {
			if !errors.Is(err, ErrNoValue) {
				err = e
			}
		} else {
			rv.SetString(x)
		}
	case reflect.Int64:
		if rt != typeDuration { //time.Duration
			if x, e := src.Duration(); e == nil {
				rv.SetInt(int64(x))
			} else if !errors.Is(e, ErrNoValue) {
				err = e
			}
			return
		}
		fallthrough
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
		if x, e := src.Int(); e == nil {
			rv.SetInt(x)
		} else if !errors.Is(e, ErrNoValue) {
			err = e
		}
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		if x, e := src.Uint(); e == nil {
			rv.SetUint(x)
		} else if !errors.Is(e, ErrNoValue) {
			err = e
		}
	case reflect.Float64, reflect.Float32:
		if x, e := src.Float(); e == nil {
			rv.SetFloat(x)
		} else if !errors.Is(e, ErrNoValue) {
			err = e
		}
	case reflect.Bool:
		if x, e := src.Bool(); e == nil {
			rv.SetBool(x)
		} else if !errors.Is(e, ErrNoValue) {
			err = e
		}
	}

	return
}
